昨天我們成功建立了完整的路徑查找系統,包括鄰接表建立、起點識別和BFS路徑查找演算法。
今天我們要繼續完成路徑高亮功能的最後一個重要環節:視覺化高亮與使用者體驗優化。
讓找到的學習路徑能夠以美觀、直覺的方式呈現在使用者面前,讓學習者一眼就能看出要達到目標需要經過哪些步驟。
當我們找到路徑後,需要對應的SVG元素進行視覺化處理。在我們的DOT語法中,每條邊都有一個唯一的ID:
// 在生成DOT語法時,為每條邊設定ID
dot += ` "${src}" -> "${tgt}" [id="${src}_${tgt}"];\n`;
然後我們可以通過這個ID找到對應的SVG元素:
/**
* 高亮單一路徑的函數
* @param {Array} path - 要高亮的路徑節點陣列,例如 ['A', 'B', 'C']
*/
function highlightSinglePath(path) {
// 檢查路徑是否有效:至少需要兩個節點才能形成一條邊
if (path.length < 2) return;
// 取得SVG容器元素,這是我們要操作的可視化圖形
const svg = zoomInner.querySelector('svg');
if (!svg) return;
// 遍歷路徑中的每一條邊(相鄰節點之間的連接)
for (let i = 0; i < path.length - 1; i++) {
const source = path[i]; // 當前邊的起始節點
const target = path[i + 1]; // 當前邊的目標節點
// 根據Graphviz的命名規則,邊的ID格式為 "起始節點_目標節點"
const edgeId = `${source}_${target}`;
const edgeElement = svg.querySelector(`#${edgeId}`);
// 如果找到了對應的邊元素
if (edgeElement) {
// 取得邊的主要視覺元素
const pathElement = edgeElement.querySelector('path'); // 邊的線條
const polygonElement = edgeElement.querySelector('polygon'); // 箭頭元素
// 如果存在線條元素,對其應用高亮樣式
if (pathElement) {
// 設定邊的顏色為亮綠色,使用 'important' 確保樣式優先級
pathElement.style.setProperty('stroke', '#00ff00', 'important');
// 增加邊的粗細,讓高亮效果更明顯
pathElement.style.setProperty('stroke-width', '10px', 'important');
// 添加脈衝動畫效果,讓路徑更加醒目
pathElement.style.setProperty('animation', 'pathPulse 2s infinite', 'important');
// 如果存在箭頭元素,也要對其應用相同的高亮樣式
if (polygonElement) {
// 設定箭頭的填充顏色
polygonElement.style.setProperty('fill', '#00ff00', 'important');
// 設定箭頭的邊框顏色
polygonElement.style.setProperty('stroke', '#00ff00', 'important');
}
}
}
}
}
我們添加了脈衝動畫效果,讓高亮的路徑更加醒目:
@keyframes pathPulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
.path-highlighted {
stroke: #00ff00 !important;
stroke-width: 10px !important;
animation: pathPulse 2s infinite !important;
}
這個動畫會讓高亮的路徑產生呼吸般的脈衝效果,讓使用者更容易注意到。
當有多條路徑時,我們可以同時高亮所有路徑:
function highlightAllPaths(allPaths) {
// 先清除之前的高亮
clearPathHighlight();
if (allPaths.length === 0) return;
// 高亮每條路徑
allPaths.forEach((path, index) => {
highlightSinglePath(path);
});
}
當使用者點擊節點時,完整的互動流程如下:
node.addEventListener('click', (e) => {
e.stopPropagation();
const svgNodeId = node.id;
const nodeLabel = node.querySelector('text')?.textContent || svgNodeId;
// 通過標籤找到實際的節點 ID
const actualNodeDetail = state.nodeDetails.find(n => n.label === nodeLabel);
const actualNodeId = actualNodeDetail ? actualNodeDetail.id : svgNodeId;
// 查找並高亮所有路徑
const allPaths = findPathToNode(actualNodeId);
if (allPaths.length > 0) {
highlightPath(allPaths);
} else {
clearPathHighlight();
}
// 顯示節點資訊
showNodeInfo(actualNodeId, nodeLabel);
});
我們提供了一個「清除路徑高亮」按鈕,讓使用者可以隨時清除高亮效果:
function clearPathHighlight() {
state.highlightedPath.forEach(highlightedItem => {
if (highlightedItem.edge) {
highlightedItem.edge.classList.remove('path-highlighted');
if (highlightedItem.path) {
highlightedItem.path.style.removeProperty('stroke');
highlightedItem.path.style.removeProperty('stroke-width');
highlightedItem.path.style.removeProperty('animation');
}
if (highlightedItem.polygon) {
highlightedItem.polygon.style.removeProperty('fill');
highlightedItem.polygon.style.removeProperty('stroke');
}
}
});
state.highlightedPath = [];
}
為了讓使用者體驗更加完整,我們在節點資訊面板中添加了路徑相關的資訊:
function showNodeInfo(nodeId, nodeLabel) {
// ... 現有的節點資訊顯示邏輯 ...
// 添加路徑資訊
const allPaths = findPathToNode(nodeId);
if (allPaths.length > 0) {
const pathInfo = document.createElement('div');
pathInfo.className = 'path-info';
pathInfo.innerHTML = `
<h4>學習路徑 (${allPaths.length} 條路徑)</h4>
<div class="path-list">
${allPaths.map((path, index) => `
<div class="path-item">
<span class="path-number">路徑 ${index + 1}:</span>
<span class="path-nodes">${path.join(' → ')}</span>
</div>
`).join('')}
</div>
`;
nodeInfoContent.appendChild(pathInfo);
}
}
完成這些功能後,GASO 平台現在具備了完整的路徑高亮能力:
在這兩天的實作中,我們成功整合了多個重要的技術概念:
透過這兩天的實作,我們成功將靜態的學習地圖轉化為動態的學習導航系統。
這個轉變的意義不僅僅是技術上的進步,更重要的是學習體驗的革新:
明天我們將繼續優化這個平台,讓學習體驗變得更加豐富和有趣。
我們明天見!
如果想要看一些我的鐵人賽花邊心得,
也歡迎追蹤我的 Threads 和 Facebook